

/**
 * Each series gets its own axis
 *
 * @type       {boolean}
 */
var KEY_OWN_AXIS = "chart.ownaxis";
var KEY_ROLLING_WINDOW = "chart.rollingwindow";
var KEY_PERCENTILE = "chart.percentile";
var KEY_PERCENTILE_EXPANSION = "chart.percentile.expansion";
var KEY_MOUSE_INACTIVE_TIMER = "chart.inactivetimer";
var KEY_FRAME_COLOR = "chart.frame.color";


/**
 * The interval in milliseconds to automatically recalculate
 * the %tile of each Y-Axis
 *
 * @type       {integer}
 */
var KEY_PERCENTILE_INTERVAL = "percentile.recalc.interval";

/**
 * During a live window, the number of recent points to keep in a %tile 
 * calculation so that it will not obscure the most recent snapshot.
 *
 * @type       {integer}
 */
var KEY_PERCENTILE_KEEP = "liveview.include.recent.points";

/**
 * The business hours to filter data points.  Data oustide this range will be filtered.
 * example: 09:30-11:00,13:00-15:30
 *
 * @type       {string}
 */
var KEY_BUSINESS_HOURS = "business.hours";
var KEY_STATIC_START_OFFSET = "chart.xaxis.viewable.offset";

/**
 * When to enable data grouping, where approximation starts to take
 * place to improve rendering performance.
 * @deprecated unused
 *
 * @type       {integer}
 */
var KEY_APPROXIMATION_POINTS = "data.grouping.points"

/**
 * The grouping interval width, once points take more than this pixel amount, grouping will
 * be in place at that location.
 * @type {float}
 */
var KEY_DATAGROUPING_WIDTH = "data.grouping.width";

/**
 * Explicitly Disable/enable datagrouping
 * @type {string}
 */
var KEY_DATAGROUPING_ENABLED = "data.grouping.enabled";

var KEY_OHLC_START = "ohlc.start";

/**
 * ohlc/candlestick 
 */
var KEY_OHLC_CHART_TYPE = "ohlc.chartType";

/**
 * OHLC sampling period 
 */
var KEY_OHLC_PERIOD = "ohlc.period";

/**
 * Relax the datagrouping so more detail can appear when using ohlc mode
 * @type {boolean}
 */
var KEY_OHLC_DATAGROUPING_RELAXED = "ohlc.grouping.relaxed";


/**
 * own = every series gets its own axis
 * single = shared axis
 * intelligent = use percentiles to detect overlap
 * @type {string}
 */
var KEY_AXIS_TYPE = "chart.axis";

var AXIS_TYPE_INTELLIGENT = "intelligent";
var AXIS_TYPE_OWN = "own";
var AXIS_TYPE_SHARED = "shared";

/**
 * Overlap percentage to keep series on the same axis
 * 0.0 - 1.0
 * @type {string}
 */
var KEY_AXIS_OVERLAP_PCT = "chart.axis.overlap";

/**
 * The number of seconds the chart will start calculating the intelligent y axis
 * Default is two minutes
 */
var KEY_AXIS_INTELLIGENT_INTERVAL = "chart.axis.intelligent.interval";

/**
 * Threshold percentage to act,
 * KEY_AXIS_OVERLAP_PCT + KEY_AXIS_OVERLAP_THRESHOLD_PCT -> series joins the axis
 * KEY_AXIS_OVERLAP_PCT - KEY_AXIS_OVERLAP_THRESHOLD_PCT -> series splits from the axis
 * @type {string}
 */
var KEY_AXIS_OVERLAP_THRESHOLD_PCT = "chart.axis.threshold";

/**
 * Do not allow a series to move if it has moved within this many seconds
 * @type {string}
 */
var KEY_AXIS_COOLDOWN = "chart.axis.cooldown";

/**
 * Force data grouping to show these points
 * @type {string}
 */
var KEY_GROUP_SHOW_LAST_POINTS = "data.grouping.showpoints";

/**
 * Seconds where we will force the daily bloomberg view, defaults to one
 */
var KEY_BBG_DAILY_THRESHOLD = "daily.category.threshold";

var KEY_CHART_FONT_SIZE= "chart.fontsize";

var SERIES_TYPE_OHLC = "ohlc";
var SERIES_TYPE_CANDLESTICK = "candlestick";


var PARAMETERS_BASIC = [
KEY_ROLLING_WINDOW, KEY_PERCENTILE, KEY_PERCENTILE_EXPANSION, KEY_PERCENTILE_INTERVAL, KEY_PERCENTILE_KEEP, KEY_BUSINESS_HOURS, KEY_AXIS_TYPE, KEY_AXIS_OVERLAP_PCT,
KEY_GROUP_SHOW_LAST_POINTS
];

var PARAMETERS_INTELLIGENT = [
	KEY_AXIS_OVERLAP_PCT, KEY_AXIS_INTELLIGENT_INTERVAL, KEY_AXIS_OVERLAP_THRESHOLD_PCT, KEY_AXIS_COOLDOWN
];



var defaultOptions = {};
defaultOptions[ KEY_PERCENTILE ] = .95;
defaultOptions[ KEY_PERCENTILE_EXPANSION ] = .1;
defaultOptions[ KEY_PERCENTILE_INTERVAL ] = 300;
defaultOptions[ KEY_PERCENTILE_KEEP ] = 5;
defaultOptions[ KEY_APPROXIMATION_POINTS ] = 2000;
defaultOptions[ KEY_MOUSE_INACTIVE_TIMER ] = 10;
defaultOptions[ KEY_DATAGROUPING_WIDTH ] = 2;
defaultOptions[ KEY_DATAGROUPING_ENABLED ] = true;
defaultOptions[ KEY_AXIS_OVERLAP_PCT ] = 0.9;
defaultOptions[ KEY_AXIS_OVERLAP_THRESHOLD_PCT ] = .05;
defaultOptions[ KEY_AXIS_COOLDOWN ] = 60;
defaultOptions[ KEY_GROUP_SHOW_LAST_POINTS ] = 5;
defaultOptions[ KEY_FRAME_COLOR ] = "gray";
defaultOptions[ KEY_OHLC_DATAGROUPING_RELAXED ] = true;

//try to squeeze 15 (2 business weeks) days into daily
defaultOptions[ KEY_BBG_DAILY_THRESHOLD ] = 15 * 24 * 60 * 60;
defaultOptions[ KEY_AXIS_INTELLIGENT_INTERVAL ] = 120;

defaultOptions[ KEY_OHLC_CHART_TYPE ] = "OHLC";
defaultOptions[ KEY_CHART_FONT_SIZE ] = 10;

var defaultAxisColor = '#6D869F';


Array.prototype.extend = function (other_array) {
  /* you should include a test to check whether other_array really is an array */
  other_array.forEach(function (v) {
    this.push(v)
  }, this);
}

//below probably goes into a util file

/**
 * @private
 */
var _MINUTE_OFFSET = 1000 * 60;
/**
 * @private
 */
var _HOUR_OFFSET = _MINUTE_OFFSET * 60;
/**
 * @private
 */
var _DAY_OFFSET = _HOUR_OFFSET * 24;
/**
 * @private
 */
var _WEEK_OFFSET = _DAY_OFFSET * 7;
/**
 * @private
 */
var _MONTH_OFFSET = _DAY_OFFSET * 30;
/**
 * @private
 */
var _YEAR_OFFSET = _DAY_OFFSET * 365;

/**
 * For live windows, give absolute offset time
 * @param count
 * @param type
 * @returns {*}
 */
function getTimeOffset( count, type ) {
	var baseOffset = 0;
	switch ( type ) {
		case "all" : return null;
		case "minute" : baseOffset = _MINUTE_OFFSET; break;
		case "hour" : baseOffset = _HOUR_OFFSET; break;
		case "day" : baseOffset = _DAY_OFFSET; break;
		case "business day" :
			var target = new Date();
			var millis = target.getTime();
			//this is live window based, assume going back in
			//time, matters for business date calculation
			manipulateDate( target, -1 * count + "b" );
			return Math.abs( millis-target.getTime() );
		case "week" : baseOffset = _WEEK_OFFSET; break;
		case "month" : baseOffset = _MONTH_OFFSET; break;
		case "year" : baseOffset = _YEAR_OFFSET; break;
	}

	return baseOffset * count;
}

function getRangeType( parameter ) {
    var targetType = null;
    if ( parameter.endsWith( "M" ) ) targetType = "minute";
    else if ( parameter.endsWith( "h" ) ) targetType = "hour";
    else if ( parameter.endsWith( "d" ) ) targetType = "day";
	else if ( parameter.endsWith( "b" ) ) targetType = "business day";
    else if ( parameter.endsWith( "w" ) ) targetType = "week";
    else if ( parameter.endsWith( "m" ) ) targetType = "month";
    else if ( parameter.endsWith( "y" ) ) targetType = "year";

    return targetType;
}

function getLiveWindowLabel( count, type ) {
	var label = ( count ? count + " " : "" ) + type;
	if ( isDefined( count ) && count > 1 ) {
		label = label + "s";
	}
	return label;
}

function mergeOptions( parameterObject ) {
	var mergedOptions = $.extend( {}, defaultOptions );
	var index;
	if ( parameterObject ) {
		var trimmedParameters = {};
		for ( var key in parameterObject ) {
			trimmedParameters[ key.trim() ] = parameterObject[ key ];
		}
		
		$.extend( mergedOptions, trimmedParameters )
	}

	return mergedOptions;
}

function convertDate( dateValue ) {
	var splitValues;
	var returnValue;
	var tempValue;
	if ( !dateValue || dateValue.indexOf( ":") < 0 ) return null;
	returnValue = new Date();
	splitValues = dateValue.trim().split( ":" );

	tempValue = 0;
	if ( splitValues.length >= 1 ) tempValue = parseInt( splitValues[0] );
	returnValue.setHours( tempValue );
	tempValue = 0;
	if ( splitValues.length >= 2 ) tempValue = parseInt( splitValues[1] );
	returnValue.setMinutes( tempValue );
	tempValue = 0;
	if ( splitValues.length >= 3 ) tempValue = parseInt( splitValues[2] );
	returnValue.setSeconds( tempValue );
	tempValue = 0;
	returnValue.setMilliseconds( tempValue );
	return returnValue;

}

function parseBusinessHours( options ) {
	var incomingParameter = options[ KEY_BUSINESS_HOURS ];
	var parsedHours = [];
	var ranges;
	var rangeCount = 0;
	var startEnd;
	var startString;
	var endString;

	if ( !incomingParameter ) return;

	ranges = incomingParameter.split( ",");
	for ( rangeCount = 0; rangeCount < ranges.length; rangeCount++ ) {
		startEnd = ranges[ rangeCount ].split( "-" );
		if ( startEnd.length != 2 ) {
			console.log( "Business Hours incorrect: ", incomingParameter );
			return;
		}
		startString = startEnd[ 0 ];
		endString = startEnd[ 1 ];
		parsedHours.push( {
			start: convertDate( startString ),
			end: convertDate( endString )
		});
		console.log( "Only Accepting data for business hours:" , parsedHours[parsedHours.length - 1]);
	}

	options.businessHours = parsedHours;
}


/**
 * Adjust the date from with the offset such that -3h into 3 hours before the date
 * If there is an unexpected value, no manipulation will occur
 */
function manipulateDate( date, offset ) {
	if ( !offset ) return;
	var trimmed = offset.trim();
	if ( trimmed.length == 0 || trimmed.length < 2 ) return;
	if ( trimmed.startsWith( "-" ) && trimmed.length < 3 ) return;
	
	var suffix = trimmed.slice( trimmed.length -1 );
	var amount = parseInt( trimmed.slice( 0, trimmed.length - 1 ) );
	//nothing to do
	if ( amount == 0) return;
	switch ( suffix ) {
		case "b":
			var increment = amount > 0 ? +1 : -1;
			var positiveAmount = Math.abs( amount );
			for ( var count = 1; count <= positiveAmount; count++ ) {
				date.setDate( date.getDate() + increment );
				while( date.getDay() == 0 || date.getDay() == 6 ) {
					date.setDate( date.getDate() + increment );
				}
			}
			break;
		case "h":
			date.setHours( date.getHours() + amount );
			break;		
		case "M":
			date.setMinutes( date.getMinutes() + amount );
			break;
		case "s":
			date.setSeconds( date.getSeconds() + amount );
			break;
		case "d":
			date.setDate( date.getDate() + amount );
			break;
		case "w":
			date.setDate( date.getDate() + amount * 7 );
			break;			
		case "m":
			date.setMonth( date.getMonth() + amount );
			break;
		case "y":
			date.setFullYear( date.getFullYear() + amount );
			break;
		default:
		 	return;
	}
	return;
}


function isDefined( value ) {
	return typeof value != "undefined";
}

function isUndefined( value ) {
	return !isDefined( value );
}

function isNotValued( value ) {
	return !isValued( value );
}

function isValued( value ) {
  return isDefined( value ) && value != null;
}

function DateUtils() {

	this.MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
  this.MILLIS_PER_MONTH = this.MILLIS_PER_DAY * 30;
  this.MILLIS_PER_YEAR = this.MILLIS_PER_DAY * 365;
  this.FORMAT_MONTH_DAY_YEAR = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric"  });
}

var _dateUtils = new DateUtils();
var DATE_UTILS = _dateUtils;

DateUtils.prototype.getMidnight = function( millis ) {
	var date = new Date( millis );
	date.setHours( 0 );
	date.setMinutes( 0 );
	date.setSeconds( 0 );
	date.setMilliseconds( 0 );

	return date.getTime();
}

DateUtils.prototype.setMidnight = function( date ) {
  date.setHours( 0 );
  date.setMinutes( 0 );
  date.setSeconds( 0 );
  date.setMilliseconds( 0 );

  return date;
}

DateUtils.prototype.setFirstOfMonth = function( date ) {
  this.setMidnight( date );
  date.setDate( 1 );

  return date;
}

DateUtils.prototype.setFirstOfYear = function( date ) {
  this.setFirstOfMonth( date );
  date.setMonth( 0 );

  return date;
}

DateUtils.prototype.addDay = function( date ) {
  date.setDate( date.getDate() + 1 );
  return date;
}

DateUtils.prototype.addMonth = function( date ) {
  date.setMonth( date.getMonth() + 1 );
  return date;
}

DateUtils.prototype.addYear = function( date ) {
  date.setYear( date.getFullYear() + 1 );
  return date;
}


function getParameterByName(name) {
  name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
  var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
    results = regex.exec(location.search);
  return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}

function getBoolean( value ) {
	if ( value == null ) return false;
	if ( typeof value == "boolean" ) return value;
  var lowerCase = value.toLowerCase().trim();

	return lowerCase == "true" || lowerCase == "y" || lowerCase == "yes";
}

function getDefaultFontSize() {
  return "10px";
}

function getDefaultFontFamily() {
  return "Tahoma, sans-serif";
}

/**
 * Number utilities
 * @constructor
 */
function NumberUtils() {

}
var NUMBER_UTILS = new NumberUtils();
var _DEFAULT_SIGNIFICANT = 4;

NumberUtils.prototype.createFormatter = function ( significantDigits ) {
  return new Intl.NumberFormat("en-US", {
    minumumFractionDigits: 0 ,
    maximumFractionDigits: significantDigits,
    maximumSignificantDigits: significantDigits
  });
}

NumberUtils.prototype.randomInterval = function ( min, max ) {
	return Math.floor(Math.random() * (max - min + 1) + min);
};

var _formatterMap = {};
_formatterMap[_DEFAULT_SIGNIFICANT] = NUMBER_UTILS.createFormatter( _DEFAULT_SIGNIFICANT );

NumberUtils.prototype.formatSignificant = function( value, numDigits ) {
  var numValue = value;
  if ( typeof value == "string") {
    numValue = parseFloat( value.trim() );
    if ( isNaN( numValue ) ) return value;
  }
  var formatter = _formatterMap[ numDigits ];
  if ( !isDefined( formatter ) ) {
    formatter = this.createFormatter( numDigits );
    _formatterMap[ numDigits ] = formatter;
  }

  return formatter.format( numValue );
}


function StringUtils() {

	this.isEqual = function( string1, string2 ) {
		if ( isNotValued( string1 ) && isNotValued( string2 ) ) return true;
		if ( isNotValued( string1 ) || isNotValued( string2 ) ) return false;
		return string1.trim().toLowerCase() == string2.trim().toLowerCase();
	}

	this.isNotEqual = function( string1, string2 ) {		
		return !this.isEqual( string1, string2 );
	}

	this.toLowerCase = function( string1 ) {
		if ( isNotValued( string1 ) ) return string1;
		return string1.toLowerCase();
	}

	this.leftPad = function( string1, character, amount ) {
		var returnValue = string1;
		while ( returnValue.length < amount ) {
			returnValue = character + returnValue;
		}
		return returnValue;
	}	
}



var STRING_UTILS = new StringUtils();

function ArrayUtils() {
  this.shuffle = function (array) {
	if (!isValued(array)) {
		return null;
	}
	var currentIndex = array.length, temporaryValue, randomIndex;

	// While there remain elements to shuffle...
	while (0 !== currentIndex) {

		// Pick a remaining element...
		randomIndex = Math.floor(Math.random() * currentIndex);
		currentIndex -= 1;

		// And swap it with the current element.
		temporaryValue = array[currentIndex];
		array[currentIndex] = array[randomIndex];
		array[randomIndex] = temporaryValue;
	}

	return array;
	}
	
	this.printArray = function( array ) {
		for (x in array) {
			if (array[x] instanceof Array) {
				array[x] = "[" + array[x].join(",") + "]";
			}
		}
		return "[" + array.join(",") + "]";
	}
}

var ARRAY_UTILS = new ArrayUtils();


if ( typeof logger == 'undefined' ) {
	window.logger = console;
}


function ObjectUtils() {

	/**
	 * Walk the javascript object, where the fn will be called with the key
	 * and if the value is a collection will be walked recursively
	 *
	 * @param      {<type>}    object  The object
	 * @param      {Function}  fn      The function, this will be the object you are inspecting, first parameter will be key of the property
	 * @return     {<type>}    { description_of_the_return_value }
	 */
	this.walkTree = function( object, fn ) {
    if ( object == null )  return object;
    var utils = this;

    for ( var property in object ) {
        var value = object[ property ];
        if ( Array.isArray( value ) ) {
        	value.forEach( function( entry ) {
        		utils.walkTree( entry, fn );
        	});
        } else if ( typeof value != "string" && typeof value != "number") {
        	utils.walkTree( value, fn );
        }
        fn.call( object, property );
    }
    return object;
}	
}

var OBJECT_UTILS =  new ObjectUtils();



String.prototype.format = String.prototype.f = function() {
	var s = this,
	  i = arguments.length;
  
	while (i--) {
	  s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
	}
	return s;
  };
  
